"""

A `Ext Direct`_ server side implementation.

Usage
-----

Instanciate a ExtDirectController and set it as a 'extdirect' attribute
on your ws root controller.

.. code-block:: python

    class MyRoot(WebServicesRoot):
        def __init__(self, *args, **kw):
            super(MyRoot, self).__init__(*args, **kw)

            extdirect = ExtDirectController(namespace="MyApplication.api")

            @wsexpose(int)
            @wsvalidate(int, int)
            def multiply(self, a, b):
                return a * b

        # [...]

Assuming your webservice root controller is mounted on the "/ws" path,
you can now use your api like this:

.. code-block:: html

    <head>
        [...]
        <script type="text/javascript" src="/ws/extdirect/provider"></script>
    </head>
    <body>
        <script type="text/javascript">
            MyApplication.api.multiply(1, 2, function(p, response) {
                if(response.type == 'exception') {
                    alert("Sorry it failed");
                } else {
                    alert("Result is : " + response.result);
                }
            });
        </script>
    </body>

.. warning::
    
    The above examples have not been tested.

.. _Ext Direct: http://www.sencha.com/products/extjs/extdirect

"""

from turbogears import controllers, expose, config
import turbogears as tg
import cherrypy
import simplejson
import traceback
import sys

import inspect

from turbojson import jsonify


class ExtDirectController(controllers.Controller):
    api_template = """
Ext.ns("%(namespace)s");
%(namespace)s.Descriptor = %(provider)s;
"""

    provider_template = api_template + """
Ext.onReady(function() {
  Ext.Direct.addProvider(%(namespace)s.Descriptor);
});
"""

    def __init__(self, root, namespace='TGWS', rootname='ws'):
        self.root = root
        self.base_ns = namespace

        self.rootname = rootname
        self.namespaces = {
            '': {}
        }
        self.inspect_controller(root)

        self.indent = None

    def inspect_controller(self, controller, action='', prefix=''):
        methods = []
        for key in dir(controller):
            if key.startswith('_'):
                continue
            item = getattr(controller, key)

            if inspect.ismethod(item) and hasattr(item, "_ws_func_info"):
                methods.append({
                    'name': key,
                    'len': len(item._ws_func_info.params)
                })

            elif hasattr(item, "_ws_gather_functions_and_types"):
                if prefix:
                    newprefix = prefix + "." + action
                else:
                    newprefix = action

                if newprefix not in self.namespaces:
                    self.namespaces[newprefix] = {}

                self.inspect_controller(item, key, newprefix)

        if methods:
            self.namespaces[prefix][action or self.rootname] = dict(
                controller=controller,
                methods=methods)

    def listMethods(self, controller):
        methods = []
        for key in dir(controller):
            if key.startswith("_"):
                continue
            item = getattr(controller, key)
            if inspect.ismethod(item) and hasattr(item, "_ws_func_info"):
                methods.append({
                    'name': key,
                    'len': len(item._ws_func_info.params)
                })
        return methods
                
    def getprovider(self, namespace):
        actions = self.namespaces[namespace]
        if not actions:
            return None
        import cherrypy
        cherrypy.request.base
        url = cherrypy.request.base + tg.url("/api/extdirect/router/")
        if namespace:
            url += namespace
            namespace = "%s.%s" % (self.base_ns, namespace)
        else:
            namespace = self.base_ns
        return dict(
            url = url,
            type = "remoting",
            namespace = namespace,
            actions = dict(
                (name, action['methods'])
                    for name, action in actions.items()))
    
    @expose(content_type="text/javascript")
    def api(self, namespace=None):
        if namespace is None:
            namespaces = self.namespaces.keys()
        else:
            namespaces = [namespace]
        out = ''
        for namespace in namespaces:
            provider = self.getprovider(namespace)
            out += self.api_template % dict(
                namespace=provider['namespace'],
                provider=simplejson.dumps(provider, indent=self.indent))
        return out

    @expose(content_type="text/javascript")
    def provider(self, namespace=None):
        if namespace is None:
            namespaces = self.namespaces.keys()
        else:
            namespaces = [namespace]
        out = ''
        for namespace in namespaces:
            provider = self.getprovider(namespace)
            out += self.provider_template % dict(
                namespace=provider['namespace'],
                provider=simplejson.dumps(provider, indent=self.indent))
        return out

    @expose(content_type="application/json")
    def router(self, namespace=None):
        request = cherrypy.request
        body = request.body.read()
        transactions = simplejson.loads(body)

        multicall = isinstance(transactions, list)
        if not multicall:
            transactions = [transactions]

        namespace = self.namespaces[namespace or '']
        responses = []
        for transaction in transactions:
            tid = transaction['tid']

            tresp = {
                'tid': tid,
                'action': transaction['action'],
                'method': transaction['method']
            }

            try:
                if transaction['action'] not in namespace:
                    raise KeyError, "Not such action: " + transaction['action']

                controller = namespace[transaction['action']]['controller']

                if not hasattr(controller, transaction['method']):
                    raise KeyError, "Not such method: " + transaction['method']
                    
                method = getattr(controller, transaction['method'])

                if not method.exposed:
                    raise KeyError, "Not such method: " + transaction['method']

                args = transaction['data']
            
                kw = {}
                if args:
                    for i in xrange(0, len(args)):
                        kw[method._ws_func_info.params[i]] = args[i]
                tresp['result'] = method(_json_request=simplejson.dumps(kw), _tgws=True)
                tresp['type'] = 'rpc'
            except Exception, e:
                tresp['type'] = 'exception'
                tresp['message'] = str(e)
                tresp['where'] = '\n'.join(traceback.format_exception(*sys.exc_info()))
            responses.append(tresp)
        if not multicall:
            responses = responses[0]
        r = jsonify.encode(responses)
        return r

